/**
 * @author Tobias Boehm
 *
 * 10.05.2011
 */
package org.anddev.andengine.extension.tbsengine.client;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Map;

import org.anddev.andengine.extension.multiplayer.protocol.adt.message.client.ClientMessage;
import org.anddev.andengine.extension.multiplayer.protocol.adt.message.server.IServerMessage;
import org.anddev.andengine.extension.multiplayer.protocol.client.IServerMessageHandler;
import org.anddev.andengine.extension.multiplayer.protocol.client.SocketServerDiscoveryClient;
import org.anddev.andengine.extension.multiplayer.protocol.client.SocketServerDiscoveryClient.ISocketServerDiscoveryClientListener;
import org.anddev.andengine.extension.multiplayer.protocol.client.connector.BluetoothSocketConnectionServerConnector.IBluetoothSocketConnectionServerConnectorListener;
import org.anddev.andengine.extension.multiplayer.protocol.client.connector.ServerConnector;
import org.anddev.andengine.extension.multiplayer.protocol.client.connector.SocketConnectionServerConnector;
import org.anddev.andengine.extension.multiplayer.protocol.client.connector.SocketConnectionServerConnector.ISocketConnectionServerConnectorListener;
import org.anddev.andengine.extension.multiplayer.protocol.exception.WifiException;
import org.anddev.andengine.extension.multiplayer.protocol.shared.BluetoothSocketConnection;
import org.anddev.andengine.extension.multiplayer.protocol.shared.IDiscoveryData.DefaultDiscoveryData;
import org.anddev.andengine.extension.multiplayer.protocol.shared.SocketConnection;
import org.anddev.andengine.extension.multiplayer.protocol.util.IPUtils;
import org.anddev.andengine.extension.multiplayer.protocol.util.WifiUtils;
import org.anddev.andengine.extension.tbsengine.menu.activity.AbsPrepareMatchActivity;
import org.anddev.andengine.extension.tbsengine.message.server.DefaultTBSserverMessageFlags;

import android.content.Context;
import android.os.AsyncTask;

/**
 * This class represents an interface for the communication with a game server.
 * You should create and manage an instance of this class in every activity 
 * dealing with client <-> server communication. You may use this class in your
 * implementations of {@link AbsPrepareMatchActivity} and {@link AbsInGameActivity}.
 * <p>
 * At the moment there is only a constructor for IP socket connections but there
 * will be a constructor for Bluetooth connections as well.
 * 
 *  
 * @author Tobias Boehm
 *
 */
public class ClientConnectionGateway {

	/**
	 * The number of times the client attempts
	 * to connect to the server before it gives up.
	 */
	private static final int NUMBER_OF_TRIES = 60;
	
	/**
	 * The time in ms between each connection attempt.
	 */
	private static final long SLEEP_BETWEEN_TRIES = 500;
	
	/**
	 * An ip socket server connector.
	 */
	private SocketConnectionServerConnector mSocketServerConnector;
	
	/**
	 * A server ip as string.
	 */
	private String mServerIP;
	
	/**
	 * A server port as integer.
	 */
	private int mServerPort;
	
	/**
	 * A UDP socket server discovery client.
	 */
	private SocketServerDiscoveryClient<DefaultDiscoveryData> mSocketDiscoveryClient;
	
	/**
	 * A connection status listener.
	 */
	private final IOnGameClientConnectionStatusListener mGameServerConnectionStatusListener;
	
	/**
	 * A map of FLAGs and .Class'es 
	 * 
	 * @see DefaultTBSserverMessageFlags
	 */
	private final Map<Short,Class<? extends IServerMessage>> mFlagsServerMessageClassesMap;
	
	/**
	 * A map of FLAGs and {@link IServerMessageHandler}s which handle the 
	 * messages of the classes defined by the flags.
	 * 
	 * @see #mFlagsServerMessageClassesMap
	 * @see DefaultTBSserverMessageFlags
	 */
	private final Map<Short,IServerMessageHandler> mFlagsServerMessageHandlersMap;

	/**
	 * A flag which indicates whether or not the client is connected to a 
	 * game server.
	 */
	private boolean mConnected;
	
	/**
	 * Use this constructor if you want to create a connection to an IP socket
	 * game server. Look for other constructors to connect, for example to a
	 * Bluetooth game server.
	 *  
	 * @param pGameServerConnectionStatusListener
	 * @param pServerIP
	 * @param pServerPort
	 * @param pFlagsServerMessageClassesMap @see {@link #mFlagsServerMessageClassesMap}
	 * @param pFlagsServerMessageHandlersMap @see {@link #mFlagsServerMessageHandlersMap}
	 */
	public ClientConnectionGateway(final IOnGameClientConnectionStatusListener pGameServerConnectionStatusListener,
								   final String pServerIP, final int pServerPort,
								   final Map<Short,Class<? extends IServerMessage>> pFlagsServerMessageClassesMap,
								   final Map<Short,IServerMessageHandler> pFlagsServerMessageHandlersMap){
		
		this.mGameServerConnectionStatusListener = pGameServerConnectionStatusListener;
		
		this.mFlagsServerMessageClassesMap = pFlagsServerMessageClassesMap;
		
		this.mFlagsServerMessageHandlersMap = pFlagsServerMessageHandlersMap;
		
		this.mServerIP = pServerIP;
		
		this.mServerPort = pServerPort;
	}
	
	/**
	 * Use this constructor if you want to create a connection to an local WIFI
	 * socket server. Look for other constructors to connect, for example to a
	 * bluetooth game server.
	 *  
	 * @param pGameServerConnectionStatusListener
	 * @param pFlagsServerMessageClassesMap @see {@link #mFlagsServerMessageClassesMap}
	 * @param pFlagsServerMessageHandlersMap @see {@link #mFlagsServerMessageHandlersMap}
	 * @param pContext
	 * 
	 * @throws WifiException 
	 * @throws UnknownHostException 
	 */
	public ClientConnectionGateway(final IOnGameClientConnectionStatusListener pGameServerConnectionStatusListener,
								   final Map<Short,Class<? extends IServerMessage>> pFlagsServerMessageClassesMap,
								   final Map<Short,IServerMessageHandler> pFlagsServerMessageHandlersMap,
								   final Context pContext) throws UnknownHostException, WifiException{
		
		this.mGameServerConnectionStatusListener = pGameServerConnectionStatusListener;
		
		this.mFlagsServerMessageClassesMap = pFlagsServerMessageClassesMap;
		
		this.mFlagsServerMessageHandlersMap = pFlagsServerMessageHandlersMap;
		
		this.mSocketDiscoveryClient = new SocketServerDiscoveryClient<DefaultDiscoveryData>(WifiUtils.getBroadcastIPAddressRaw(pContext), DefaultDiscoveryData.class, new SocketServerDiscoveryClientListener());
	}
	
	
	/**
	 * This method tries to send a client message to the game server.
	 * 
	 * @param pClientMessage
	 * 
	 * @throws IOException
	 * @throws IllegalStateException if the connection is not established.
	 */
	public void sendClientMessage(final ClientMessage pClientMessage) throws IOException{
		
		if(mSocketServerConnector != null){
			
			this.mSocketServerConnector.sendClientMessage(pClientMessage);
		
		}else{
			
			throw new IllegalStateException("Connection not jet established. Please make sure you called connectToServer() before!");
		}
	}
		
	/**
	 * This method starts an asynchronous task that tries to connect to the
	 * game server. The task will attempt to connect to the game server for 30 
	 * seconds. If it is not successful it will call 
	 * {@link IOnGameClientConnectionStatusListener#onSocketGameServerNotReachable(String, int)}
	 * on the the {@link IOnGameClientConnectionStatusListener} you have
	 * registered by creating this instance.
	 * <p>
	 * Notice:<br> 
	 * The method returns immediately. You will get notified whether or not the
	 * connection was established through the {@link IOnGameClientConnectionStatusListener}
	 * you have registered by creating this instance.
	 */
	public void asyncConnect(){
			
		final AsyncTask<Void,Void,Void> delayConnectionTask = new AsyncTask<Void, Void, Void>(){

			@Override
			protected Void doInBackground(Void... params) {
				
				/*
				 * This delay is designed to give a local game server service
				 * time to start or to shut down.
				 */
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				return null;
			}
			
			/**
			 * After the delay the connection process should go on in the main
			 * thread.
			 */
			protected void onPostExecute(Void result) {
				
				if(mSocketDiscoveryClient != null){
								
					mSocketDiscoveryClient.discoverAsync();
					
				}else{
						
					final AsyncTask<Void, Void, SocketConnectionServerConnector> asyncConnectorTask = new AsyncTask<Void, Void, SocketConnectionServerConnector>(){

						@Override
						protected SocketConnectionServerConnector doInBackground(Void... params) {
							
							connectToIPgameServerTask();
							
							return mSocketServerConnector;
						}
						
						@Override
						protected void onPostExecute(SocketConnectionServerConnector result) {
							
							// If the server is still unreachable..
							if(mSocketServerConnector == null){
								
								mGameServerConnectionStatusListener.onSocketGameServerNotReachable(mServerIP, mServerPort);
							
							}else{
								
								mSocketServerConnector.getConnection().start();
							}
						}
					};
							
					asyncConnectorTask.execute();
				}
			};
		};
		
		delayConnectionTask.execute();		
	}
	
	/**
	 * This method disconnects the client form the server.
	 * <p>
	 * Notice:<br>
	 * You may want to send a sign-out message to the server before you call
	 * this method.
	 */
	public void disconnectFromServer(){
		
		if(this.mSocketServerConnector != null){
			
			if(this.mSocketDiscoveryClient != null){
				
				this.mSocketDiscoveryClient.terminate();
			}
			
			this.mSocketServerConnector.getConnection().terminate();
		}
	}
	
	/**
	 * @return the mConnected
	 */
	public boolean isConnected() {
		return mConnected;
	}
	
	private void onConnected(final ServerConnector pServerConnector) {
		
		for(Short typeFlag : this.mFlagsServerMessageHandlersMap.keySet()){
			
			pServerConnector.registerServerMessage(typeFlag, this.mFlagsServerMessageClassesMap.get(typeFlag), this.mFlagsServerMessageHandlersMap.get(typeFlag));
		}
		
		mGameServerConnectionStatusListener.onGameServerConnected();
		
		this.mConnected = true;
	}

	private void onDisconnected(final ServerConnector pServerConnector) {

		this.mConnected = false;
		
		mGameServerConnectionStatusListener.onGameServerDisconnected();		
	}

	private void connectToIPgameServerTask() {
					
		for(int pos = 0; pos < NUMBER_OF_TRIES; pos++){
			
			if(mSocketServerConnector == null){
				
				try {
					mSocketServerConnector = new SocketConnectionServerConnector(new SocketConnection(new Socket(mServerIP, mServerPort)), new SocketConnectionServerConnectorListener());
				} catch (UnknownHostException e) {

					try {
						Thread.sleep(SLEEP_BETWEEN_TRIES);
					} catch (InterruptedException e1) {}	
					
				} catch (IOException e) {
					try {
						Thread.sleep(SLEEP_BETWEEN_TRIES);
					} catch (InterruptedException e1) {}		
				}
			
			}else{
				break;
			}
		}
	}

	private class BluetoothSocketConnectionServerConnectorListener implements IBluetoothSocketConnectionServerConnectorListener{

		@Override
		public void onStarted(
				ServerConnector<BluetoothSocketConnection> pServerConnector) {
			
			ClientConnectionGateway.this.onConnected(pServerConnector);

		}

		@Override
		public void onTerminated(
				ServerConnector<BluetoothSocketConnection> pServerConnector) {
			
			ClientConnectionGateway.this.onDisconnected(pServerConnector);
		}
	}
	
	private class SocketConnectionServerConnectorListener implements ISocketConnectionServerConnectorListener{

		@Override
		public void onStarted(ServerConnector<SocketConnection> pServerConnector) {

			ClientConnectionGateway.this.onConnected(pServerConnector);
		}

		@Override
		public void onTerminated(
				ServerConnector<SocketConnection> pServerConnector) {
			
			ClientConnectionGateway.this.onDisconnected(pServerConnector);
		}
	}
	
	private class SocketServerDiscoveryClientListener implements ISocketServerDiscoveryClientListener<DefaultDiscoveryData>{

		@Override
		public void onDiscovery(SocketServerDiscoveryClient<DefaultDiscoveryData> pSocketServerDiscoveryClient,
				DefaultDiscoveryData pDiscoveryData) {
			
			try {
				mServerIP = IPUtils.ipAddressToString(pDiscoveryData.getServerIP());
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			mServerPort = pDiscoveryData.getServerPort();
			
			/*final AsyncTask<Void, Void, SocketConnectionServerConnector> asyncCon = createAsyncSocketConnector();
			
			asyncCon.execute();*/
			connectToIPgameServerTask();
			
			if(mSocketServerConnector == null){
				
				mGameServerConnectionStatusListener.onSocketGameServerNotReachable(mServerIP, mServerPort);
			
			}else{
				
				mSocketServerConnector.getConnection().start();
			}
		}

		@Override
		public void onTimeout(SocketServerDiscoveryClient<DefaultDiscoveryData> pSocketServerDiscoveryClient,
				SocketTimeoutException pSocketTimeoutException) {
			
			mGameServerConnectionStatusListener.onSocketGameServerNotReachable("unkown", -1);
		}

		@Override
		public void onException(SocketServerDiscoveryClient<DefaultDiscoveryData> pSocketServerDiscoveryClient,
				Throwable pThrowable) {
			
			mGameServerConnectionStatusListener.onSocketGameServerNotReachable("Exception: "+pThrowable.getMessage(), -1);
		}
	}
	
	/**
	 * This interface provides methods called by the
	 * {@link ClientConnectionGateway}. You may implement this interface in your
	 * implementation of {@link AbsPrepareMatchActivity} and
	 * {@link AbsInGameActivity}.
	 * 
	 * @author Tobias Boehm
	 *
	 */
	public interface IOnGameClientConnectionStatusListener {

		/**
		 * This method is called if the connection to the game server was
		 * established successfully.
		 */
		public void onGameServerConnected();
		
		/**
		 * This method is called if the connection to the game server is
		 * interrupted, or you close the connection manually.
		 */
		public void onGameServerDisconnected();
		
		/**
		 * This method is called if the connection build-up to the game server
		 * failed.
		 * 
		 * @param pDestinationIP The IP which was not reachable.
		 * @param pDestinationPort The port which was not reachable.
		 */
		public void onSocketGameServerNotReachable(final String pDestinationIP, final int pDestinationPort);
	}
}
